
Alkaline.JSInfo = {
	
	associativeOperators: ['*', '<<', '>>', '>>>', '&', '^', '|', '||', '&&'],
	
	// these can be collapsed with an assignment; e.g. a += 2
	collapsibleOperators: ['+', '-', '*', '/', '%', '<<', '>>', '>>>', '&', '|', '^'],
	collapsibleOperatorsHash: null,	// populated on init

	// these operate only on numbers => they coerce both arguments to be numbers
	binaryNumberOperators: ['-', '*', '/', '%', '>>', '>>>', '<<', '&', '|', '^'],
	binaryNumberOperatorsHash: null,	// populated on init
	unaryNumberOperators: ['+', '-', '~'],
	unaryNumberOperatorsHash: null,	// populated on init
	
	// these operators are always complementary (only ==, !=, === and !==). ">" and "<=" are not complementary because NaN > 4 is false and NaN <= 4 is also false
	alwaysComplementaryOperators: null,
	alwaysComplementaryOperatorsHash: null,
	alwaysComplementaryOperatorsComplement: null,

	comparisonOperators: ['==', '===', '!=', '!==', '<', '<=', '>', '>='],
	comparisonOperatorsHash: null,

	futureKeywords: ["abstract", "boolean", "byte", "char", "class", "const", "debugger", "double", "enum", "export", "extends", "final", "float", "goto", "implements", "import", "int", "interface", "long", "native", "package", "private", "protected", "public", "short", "static", "super", "synchronized", "throws", "transient", "volatile"],
	futureKeywordsHash: null,

	endsInExpression: function(tree) {
		var maybeHash = Alkaline.JSInfo.endsInExpression.maybeHash;
		var type = tree.token.type;
		while (maybeHash[type]) {
			tree = tree.children[tree.children.length-1];
			type = tree.token.type;
		}
		if (type === this.TOKEN.Function)
			return !tree.isFunctionDeclaration;
		return !!Alkaline.JSInfo.endsInExpression.hash[type];
	},

	expressionCouldBeStatement: function(tree) {
		switch (tree.parent.token.type) {
			case this.TOKEN.BLOCK:
				return true;
			case this.TOKEN.If:
				return !!tree.childIndex;
			case this.TOKEN.For:
			case this.TOKEN.While:
			case this.TOKEN.With:
				return tree.childIndex === tree.parent.children.length-1;
		}
		return false;
	},

	getExpressionRoot: function(ast) {
		while (ast.parent && Alkaline.JSInfo.isExpression(ast.parent))
			ast = ast.parent;
		return ast;
	},

	getCommentContent: function(str) {
		if (str.token) str = str.token.text;
		if (str.charAt(1) === '*') {
			str = str.substr(2, str.length-4);	// remove "/*" and "*/"
			while (true) {
				// loop because after the first replacements some lines may be skipped
				var str1 = str.replace(/^\s*\*\s*/gm, '');
				if (str === str1)
					break;
				str = str1;
			}
		} else
			str = str.substr(2);	// remove "//"
		str = str.replace(/[ \t]+/gm, ' ');
		str = str.trim();
		return str;
	},

	getCommentStringFromContent: function(str) {
		var lineStart = '\n *\t';
		str = '/**' + lineStart + str.replace(/\n\s*/g, lineStart) + '\n */';
		return str;
	},
	
	isCollapsedOperator: function(opStr) {
		return !!this.isCollapsedOperator.hash[opStr];
	},
	
	isCollapsibleOperator: function(opStr) {
		return !!this.isCollapsibleOperator.hash[opStr];
	},
	
	isExpression: function(ast) {
		return !!Alkaline.JSInfo.isExpression.typesHash[ast.token.type];
	},
	
	isLiteral: function(ast) {
		return !!Alkaline.JSInfo.isLiteral.typesHash[ast.token.type];
	},
	
	isLoop: function(ast) {
		return !!Alkaline.JSInfo.isLoop.hash[ast.token.type];
	},
	
	/**
	 *	Is this a statement which takes an expression argument?
	 *	(return, throw, default directive)
	 */
	isStatementExpression: function(ast) {
		return !!Alkaline.JSInfo.isStatementExpression.hash[ast.token.type];
	},
	
	opIsAssignment: function(opText) {
		return !!Alkaline.JSInfo.opIsAssignment.hash[opText];
	},
	
	/**
	 *	shortestNumberRepresentation
	 */
	shortestNumberRepresentation: function(text) {
		if (text.length < 2)
			// single digit
			return text;
		
		if (text.charAt(0) === '0')
			// starts with 0 but not 0 => octal or hexadecimal; convert to decimal
			text = String(Number(text, text.charAt(1) === 'x' ? 16 : 8));
		
		if (text.length > 3) {
			var p = text.length;
			// the number cannot start with 0 se we can skip checking the first digit
			while (p && text.charAt(p-1) === '0')
				p--;
			if (text.length - p > 2)
				// at least 3 of the last digits are 0
				text = text.substr(0, p) + 'e' + (text.length-p);
		}
		
		return text;
	},
	
	/**
	 *	returnValueIsUsed
	 */
	returnValueIsUsed: function(ast) {
		switch (ast.parent.token.type) {
			case Alkaline.JSInfo.TOKEN.EXPR:
				return ast.childIndex === ast.parent.children.length-1 && Alkaline.JSInfo.returnValueIsUsed(ast.parent);
			
			case Alkaline.JSInfo.TOKEN.Operator:
				if (ast.parent.token.text === "?")
					return !ast.childIndex || Alkaline.JSInfo.returnValueIsUsed(ast.parent);
				break;
		}
		
		if (Alkaline.JSInfo.returnValueIsUsed.hash[ast.parent.token.type])
			return true;
		if (ast.coercedTo && ast.coercedBy === ast.parent)
			return true;
		return false;
	},
	
	/**
	 *	stringContainsId
	 */
	stringContainsId: function(str) {
		str = this.unescapeString(str);
		return str.match(this.stringContainsId.rx);
	},
	
	/**
	 *	stringContainsIdOrNumber
	 */
	stringContainsIdOrNumber: function(str) {
		str = this.unescapeString(str);
		return str.match(this.stringContainsIdOrNumber.rx);
	},

	/**
	 *	stringToIdOrNumber
	 */
	stringToIdOrNumber: function(str, blacklist) {
		var substr = str.substr(1, str.length-2);
		if (blacklist.indexOf(',' + substr + ',') < 0 && substr.match(this.stringContainsIdOrNumber.rx))
			return substr;
		return str;
	},
	
	/**
	 *	unescapeString
	 */
	unescapeString: function(str) {
		var cache = this.unescapeString.cache;
		str = str.substr(1, str.length-2);
		str = str.replace(/\\([^ux0-9]|u....|x..|[0-9]..)/g, function(escape) {
			return cache[escape] || (cache[escape] = eval('"' + escape + '"'));
		});
		return str;
	},
	
	TOKEN: {},		// name-to-type; this is populated later
	tokenNames: [],	// list of names; this is populated later
	tokenName: {},	// type-to-name; this is populated later
	
	/**
	 *	init
	 */
	init: function(){
		var JSInfo = this;
		var TOKEN = this.TOKEN;
		var tokenNames = this.tokenNames;
		var typeToName = this.tokenName;
		for (var i = 0; i < AlkalineParser.tokenNames.length; i++) {
			var tokenType = AlkalineParser[AlkalineParser.tokenNames[i]];
			if (typeof tokenType === 'number') {
				var tokenName = AlkalineParser.tokenNames[i];
				TOKEN[tokenName] = tokenType;
				tokenNames.push(tokenName);
				typeToName[tokenType] = tokenName;
			}
		}
		
		var listToHash = function(list) {
			var hash = {};
			for (var i = 0; i < list.length; i++)
				hash[list[i]] = true;
			return hash;
		}
		
		this.futureKeywordsHash = listToHash(this.futureKeywords);

		// lists of operators:
		
		this.collapsibleOperatorsHash = listToHash(this.collapsibleOperators);
		this.binaryNumberOperatorsHash = listToHash(this.binaryNumberOperators);
		this.unaryNumberOperatorsHash = listToHash(this.unaryNumberOperators);
		this.opIsAssignment.hash = { '=': true };
		for (var i = 0; i < this.collapsibleOperators.length; i++)
			this.opIsAssignment.hash[this.collapsibleOperators[i] + '='] = true;
		
		this.alwaysComplementaryOperators = [];
		this.alwaysComplementaryOperatorsHash = {};
		this.alwaysComplementaryOperatorsComplement = {};
		var addAlwaysComplementaryPair = function(op1, op2) {
			JSInfo.alwaysComplementaryOperators.push(op1, op2);
			JSInfo.alwaysComplementaryOperatorsComplement[op1] = op2;
			JSInfo.alwaysComplementaryOperatorsComplement[op2] = op1;
		}
		addAlwaysComplementaryPair('==', '!=');
		addAlwaysComplementaryPair('===', '!==');
		this.alwaysComplementaryOperatorsHash = listToHash(this.alwaysComplementaryOperators);
		
		this.comparisonOperatorsHash = listToHash(this.comparisonOperators);
		
		// /lists of operators
		
		this.nonEmptyTokens = [
			this.TOKEN.Const,
			this.TOKEN.EXPR,
			this.TOKEN.Let,
			this.TOKEN.Var
		];
		
		this.isLiteral.types = [
			this.TOKEN.ARRAY,
			this.TOKEN.ArrayComprehension,
			this.TOKEN.False,
			this.TOKEN.Function,
			this.TOKEN.Number,
			this.TOKEN.Null,
			this.TOKEN.OBJECT,
			this.TOKEN.RegExp,
			this.TOKEN.String,
			this.TOKEN.True,
			this.TOKEN.XmlElement,
			this.TOKEN.XmlCData,
			this.TOKEN.XmlNodeList
		];
		this.isLiteral.typesHash = listToHash(this.isLiteral.types);
		
		this.isExpression.types = [
			this.TOKEN.CALL,
			this.TOKEN.EXPR,
			this.TOKEN.Delete,
			this.TOKEN.FILTER,
			this.TOKEN.Id,
			this.TOKEN.ITEM,
			this.TOKEN.New,
			this.TOKEN.Operator,
			this.TOKEN.PostOp,
			this.TOKEN.SELECT,
			this.TOKEN.TypeOf,
			this.TOKEN.UnaryOperator,
			this.TOKEN.Void,
			this.TOKEN.Yield
		].concat(this.isLiteral.types);
		this.isExpression.typesHash = listToHash(this.isExpression.types);
		
		this.isStatementExpression.list = [
			this.TOKEN.DefaultDirective,
			this.TOKEN.Return,
			this.TOKEN.Throw
		];
		this.isStatementExpression.hash = listToHash(this.isStatementExpression.list);
		
		this.returnValueIsUsed.list = [
			this.TOKEN.Case,
			this.TOKEN.In,	// for-in, for-each-in
			this.TOKEN.Switch
		].concat(this.isExpression.types).concat(this.isStatementExpression.list);
		this.returnValueIsUsed.hash = listToHash(this.returnValueIsUsed.list);
		
		Alkaline.JSInfo.isLoop.list = [
			this.TOKEN.Do,
			this.TOKEN.For,
			this.TOKEN.While,
			this.TOKEN.With
		];
		Alkaline.JSInfo.isLoop.hash = listToHash(Alkaline.JSInfo.isLoop.list);
		
		this.endsInExpression.list = this.isStatementExpression.list.concat(this.isExpression.types).concat([
			this.TOKEN.Const,
			this.TOKEN.Let,
			this.TOKEN.Var
		]);
		this.endsInExpression.hash = listToHash(this.endsInExpression.list);
		this.endsInExpression.maybeHash = listToHash([
			this.TOKEN.For,
			this.TOKEN.If,
			this.TOKEN.LABEL,
			this.TOKEN.While,
			this.TOKEN.With
		]);
		
		this.isCollapsibleOperator.hash = {};
		this.isCollapsedOperator.hash = {};
		for (var i = 0; i < this.collapsibleOperators.length; i++) {
			this.isCollapsibleOperator.hash[this.collapsibleOperators[i]] =
			this.isCollapsedOperator.hash[this.collapsibleOperators[i] + '='] =
				true;
		}
		
		this.unescapeString.cache = {
			'\\\\': '\\',
			'\\"': '"',
			"\\'": "'",
			'\\n': '\n',
			'\\r': '\r',
			'\\t': '\t'
		};
		
		this.stringContainsId.rx = /^[a-zA-Z_][a-zA-Z_0-9]*$/;
		
		this.stringContainsIdOrNumber.rx = /^([a-zA-Z_][a-zA-Z_0-9]*|0|[1-9][0-9]*)$/;
		
		
		this.init = null;
	}
}
